package crmdna.inventory;

import static crmdna.common.AssertUtils.ensureEqual;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;
import static crmdna.common.ProjectionQuery.pq;
import static crmdna.inventory.InventoryItemSaver.inventoryItemSaver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PropertyProjection;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.gson.Gson;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
import com.googlecode.objectify.cmd.QueryKeys;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.DSUtils;
import crmdna.common.UnitUtils;
import crmdna.common.UnitUtils.PhysicalQuantity;
import crmdna.common.UnitUtils.ReportingUnit;
import crmdna.group.Group;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;
import crmdna.user.User;
import crmdna.user.User.Action;
import crmdna.user.User.ResourceType;

public class InventoryItem {

	public static InventoryItemProp create(String client, long groupId,
			long inventoryItemTypeId, String displayName,
			PhysicalQuantity physicalQuantity, ReportingUnit reportingUnit,
			double initialQuantityInReportingUnit, String login) {

		Group.safeGet(client, groupId);

		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM, groupId,
				Action.WRITE);

		InventoryItemEntity entity = new InventoryItemEntity();
		entity.inventoryItemId = Sequence.getNext(client,
				SequenceType.INVENTORY_ITEM);
		entity.groupId = groupId;
		entity.displayName = displayName;
		entity.physicalQuantity = physicalQuantity;
		entity.inventoryItemTypeId = inventoryItemTypeId;
		entity.groupId = groupId;
		entity.reportingUnit = reportingUnit;
		entity.quantityInDefaultUnit = UnitUtils
				.safeGetQuantityInDefaultUnit(physicalQuantity,
						initialQuantityInReportingUnit, reportingUnit);

		inventoryItemSaver(client, entity).populateDependentsAndSave();

		return entity.toProp();
	}

	public static InventoryItemEntity safeGet(String client,
			long inventoryItemId) {

		Client.ensureValid(client);

		InventoryItemEntity entity = ofy(client).load()
				.type(InventoryItemEntity.class).id(inventoryItemId).get();
		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND,
					"Inventory item id  [" + inventoryItemId
							+ "] does not exist");

		return entity;
	}

	public static List<InventoryItemProp> query_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		List<com.google.appengine.api.datastore.Key> rawKeys = new ArrayList<>();
		for (Key key : keys) {
			rawKeys.add(key.getRaw());
		}

		System.out.println("ids = " + new Gson().toJson(ids));

		ensureEqual(keys.size(), props.size());

		AsyncDatastoreService datastore = DatastoreServiceFactory
				.getAsyncDatastoreService();

		// com.google.appengine.api.datastore.Query.Filter filter = new
		// com.google.appengine.api.datastore.Query.FilterPredicate(
		// "inventoryItemId", FilterOperator.IN, ids);

		com.google.appengine.api.datastore.Query.Filter filter = new com.google.appengine.api.datastore.Query.FilterPredicate(
				Entity.KEY_RESERVED_PROPERTY, FilterOperator.IN, rawKeys);

		com.google.appengine.api.datastore.Query q = new com.google.appengine.api.datastore.Query(
				InventoryItemEntity.class.getSimpleName());

		q.setFilter(filter).addProjection(
				new PropertyProjection("groupId", Long.class));

		Iterable<Entity> groupIds = datastore.prepare(q).asList(
				FetchOptions.Builder.withLimit(10000));

		q = new com.google.appengine.api.datastore.Query("InventoryItemEntity");
		q.setFilter(filter).addProjection(
				new PropertyProjection("inventoryItemTypeId", Long.class));

		Iterable<Entity> inventoryItemTypeIds = datastore.prepare(q)
				.asIterable();

		int i = 0;
		for (Entity entity : groupIds) {

			InventoryItemProp prop = props.get(i);
			prop.groupId = (long) entity.getProperty("groupId");
			i++;
		}
		System.out.println("i = " + i + ", key.size() = " + keys.size());

		i = 0;
		for (Entity entity : inventoryItemTypeIds) {

			InventoryItemProp prop = props.get(i);
			prop.inventoryItemTypeId = (long) entity
					.getProperty("inventoryItemTypeId");
			i++;
		}
		ensureEqual(keys.size(), i);

		System.out.println("before populateDependents. props: "
				+ new Gson().toJson(props));

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	public static List<InventoryItemProp> query(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		ensureNotNull(keys);

		Collection<InventoryItemEntity> entities = ofy(client).load()
				.keys(keys).values();

		List<InventoryItemProp> props = new ArrayList<>(keys.size());
		for (InventoryItemEntity entity : entities) {
			props.add(entity.toProp());
		}

		Collections.sort(props);
		return props;
	}

	public static List<InventoryItemProp> query2_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		String kind = "InventoryItemEntity";
		// List<Long> groupIds = DSUtils.executeProjectionQuery(kind, keys,
		// "groupId", Long.class);

		List<Long> groupIds = DSUtils.executeProjectionQuery2(
				InventoryItemEntity.class, keys, "groupId", Long.class);

		List<Long> inventoryItemTypeIds = DSUtils.executeProjectionQuery(kind,
				keys, "inventoryItemTypeId", Long.class);

		ensureEqual(keys.size(), groupIds.size(),
				"Records returned by projection query [" + groupIds.size()
						+ "] is different from number of keys [" + keys.size()
						+ "]");
		ensureEqual(keys.size(), inventoryItemTypeIds.size(),
				"Projection query result mismatch");

		for (int i = 0; i < keys.size(); i++) {
			InventoryItemProp prop = props.get(i);
			prop.groupId = groupIds.get(i);
			prop.inventoryItemTypeId = inventoryItemTypeIds.get(i);
		}

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	public static List<InventoryItemProp> query3_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		List<Long> groupIds = pq(InventoryItemEntity.class, Long.class)
				.keys(keys).property("groupId").execute();

		List<Long> inventoryItemTypeIds = pq(InventoryItemEntity.class,
				Long.class).keys(keys).property("inventoryItemTypeId")
				.execute();

		ensureEqual(keys.size(), groupIds.size(),
				"Records returned by projection query [" + groupIds.size()
						+ "] is different from number of keys [" + keys.size()
						+ "]");
		ensureEqual(keys.size(), inventoryItemTypeIds.size(),
				"Projection query result mismatch");

		for (int i = 0; i < keys.size(); i++) {
			InventoryItemProp prop = props.get(i);
			prop.groupId = groupIds.get(i);
			prop.inventoryItemTypeId = inventoryItemTypeIds.get(i);
		}

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	static QueryKeys<InventoryItemEntity> queryKeys(String client,
			InventoryItemQueryCondition qc) {

		Client.ensureValid(client);

		ensureNotNull(qc);

		Query<InventoryItemEntity> query = ofy(client).load().type(
				InventoryItemEntity.class);

		if (qc.groupId != null) {
			query = query.filter("groupId", qc.groupId);
		}

		if ((qc.inventoryItemTypeIds != null)
				&& !qc.inventoryItemTypeIds.isEmpty()) {
			query = query.filter("inventoryItemTypeId in",
					qc.inventoryItemTypeIds);
		}

		if ((qc.firstChars != null) && !qc.firstChars.isEmpty()) {
			query = query.filter("firstChar in", qc.firstChars);
		}

		return query.keys();
	}

	public static InventoryItemProp updateQuantity(String client,
			long inventoryItemId, double changeInReportingUnit,
			ReportingUnit reportingUnit, String changeDescription,
			long departmentId, boolean allowNegativeQuantity, String login) {

		InventoryItemEntity entity = safeGet(client, inventoryItemId);

		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM,
				entity.groupId, Action.UPDATE_QUANTITY);

		double changeInDefaultUnit = UnitUtils.safeGetQuantityInDefaultUnit(
				entity.physicalQuantity, changeInReportingUnit, reportingUnit);

		if (!allowNegativeQuantity) {
			if (entity.quantityInDefaultUnit + changeInDefaultUnit < 0)
				throw new APIException()
						.status(Status.ERROR_RESOURCE_INCORRECT).message(
								"Negative quantity not allowed");
		}

		entity.quantityInDefaultUnit += changeInDefaultUnit;
		inventoryItemSaver(client, entity).save();

		// record the change
		InventoryItemChangeProp prop = new InventoryItemChangeProp();
		prop.changeDescription = changeDescription;
		prop.changeInDefaultUnit = changeInDefaultUnit;
		prop.departmentId = departmentId;
		prop.inventoryItemId = inventoryItemId;
		prop.login = login;
		
		InventoryItemChange.recordChange(client, entity.groupId, prop);

		return entity.toProp();
	}
}
